13장. 실제 사례로 익히는 균형

원문 13장 "균형 잡힌 결합의 실제"를 입문자용으로 다시 풀었다.
이 장은 이론을 외우는 장이 아니라, 실제 설계 선택을 결합 균형으로 해석하는 장이다.

앞 장들에서 우리는 세 가지 질문을 반복했다.

  • 결합 세기는 얼마나 강한가?
  • 결합 거리는 얼마나 먼가?
  • 변동성은 얼마나 높은가?

13장은 이 질문을 실제 사례에 적용한다.

중요한 것은 어떤 패턴 이름을 외우는 것이 아니다.
같은 패턴도 상황에 따라 좋은 선택이 될 수도 있고 과한 선택이 될 수도 있다.

이 장에서는 8가지 사례를 보며 균형 감각을 익힌다.


0. 이 장의 새 단어

단어 아주 쉬운 뜻
사례 연구 실제 상황을 놓고 왜 그런 설계를 택했는지 보는 공부
이벤트 소싱 상태를 바로 저장하지 않고, 일어난 사건들을 기록해 상태를 재구성하는 방식
포트와 어댑터 핵심 로직과 외부 도구를 분리하는 구조
애그리거트 함께 일관성을 지켜야 하는 도메인 객체 묶음
수직 슬라이스 기능 하나를 기준으로 화면, 로직, 데이터 접근을 묶는 구조
인터페이스 분리 필요한 기능만 가진 작은 인터페이스로 나누는 원칙

1. 이 장에서 딱 5가지만

  1. 패턴은 이름보다 균형을 맞추는 이유가 중요하다.
  2. 외부 지식이 자주 바뀌면 내부 로직과 분리해야 한다.
  3. 함께 일관성을 지켜야 하는 것은 가까이 묶는다.
  4. 멀리 떨어진 부품은 안정적인 계약으로 연결한다.
  5. 폴더 구조도 결합 거리와 책임을 바꾼다.

2. 사례 1: 이벤트 소싱과 외부 지식

일반적인 저장 방식은 현재 상태를 저장한다.

{
  "orderId": "A-100",
  "status": "PAID"
}

이벤트 소싱은 상태가 아니라 사건을 저장한다.

[
  { "event": "OrderCreated", "orderId": "A-100" },
  { "event": "PaymentReceived", "orderId": "A-100" },
  { "event": "OrderPaid", "orderId": "A-100" }
]

왜 이렇게 할까?

상태를 만드는 규칙이 자주 바뀌거나, 과거에 무슨 일이 있었는지가 중요하면 사건 기록이 유리하다.

예를 들어 회계, 감사, 주문 이력처럼 "왜 이 상태가 되었는가"가 중요할 수 있다.
이때 현재 상태만 저장하면 외부 지식이 사라진다.

하지만 이벤트 소싱은 복잡하다.

  • 이벤트 순서를 이해해야 한다.
  • 과거 이벤트 형식을 유지해야 한다.
  • 재생 로직을 만들어야 한다.
  • 조회용 상태를 따로 만들 수 있다.

그러므로 이벤트 소싱은 아무 데나 쓰는 멋진 패턴이 아니다.
과거 사건 지식의 가치가 높고 변동성 관리가 필요한 곳에 쓰는 선택이다.


3. 사례 2: 충분히 좋은 통합

모든 통합을 완벽하게 만들 필요는 없다.

예를 들어 외부 배송 API와 연결한다고 하자.

선택 A:

  • 완벽한 추상화 계층
  • 모든 배송사를 동일한 모델로 변환
  • 복잡한 에러 처리
  • 많은 테스트와 매핑 코드

선택 B:

  • 지금 쓰는 배송사에 필요한 최소 계약
  • 실패 시 사용자에게 명확한 안내
  • 나중에 배송사가 늘면 확장

처음부터 배송사를 자주 바꿀 계획이 없다면 B가 더 나을 수 있다.

균형 감각은 이렇게 묻는다.

  • 이 외부 API의 변동성은 높은가?
  • 다른 배송사를 곧 추가할 가능성이 큰가?
  • 추상화 비용이 지금의 위험보다 큰가?

좋은 설계는 항상 가장 일반적인 설계가 아니다.
현재 변동성과 거리와 세기에 맞는 설계다.


4. 사례 3: 계층형 구조와 수직 슬라이스

계층형 구조는 기술 역할별로 나눈다.

controllers/
services/
repositories/
models/

장점은 분명하다.

  • 역할이 익숙하다.
  • 기술 책임이 분리된다.
  • 작은 프로젝트에서 시작하기 쉽다.

하지만 기능 하나를 고칠 때 여러 폴더를 오가야 할 수 있다.

controllers/order_controller.py
services/order_service.py
repositories/order_repository.py
models/order.py

수직 슬라이스는 기능 기준으로 묶는다.

orders/
  controller.py
  service.py
  repository.py
  model.py

payments/
  controller.py
  service.py
  repository.py
  model.py

주문 기능이 자주 함께 바뀐다면 수직 슬라이스가 거리를 줄인다.

즉 폴더 구조도 결합 거리다.
어떤 파일이 가까이 있느냐가 개발자의 이해 비용을 바꾼다.


5. 사례 4: 포트와 어댑터

핵심 로직이 외부 도구에 직접 의존하면 변동성이 새어 들어온다.

static dynamic Pay(dynamic order) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
    var response = stripe_client.Charge(order.Card, order.Total); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
    if (response.Status == "ok") // 조건이 참일 때만 아래 결정을 실행합니다.
    {
        order.MarkPaid(); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}

여기서 주문 결제 로직이 특정 결제사의 응답 구조를 직접 안다.
외부 API가 바뀌면 핵심 로직도 흔들린다.

포트와 어댑터는 핵심이 필요한 계약을 먼저 세운다.

public class PaymentPort // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic Charge(dynamic request) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        /* 구현 생략 */ // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}

핵심 로직은 포트를 사용한다.

static dynamic Pay(dynamic order, dynamic payment_port) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
    var result = payment_port.Charge(PaymentRequest.FromOrder(order)); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
    if (result.Approved) // 조건이 참일 때만 아래 결정을 실행합니다.
    {
        order.MarkPaid(); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}

특정 결제사의 세부사항은 어댑터가 맡는다.

public class StripePaymentAdapter : PaymentPort // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic Charge(dynamic request) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        var response = stripe_client.Charge(/* 구현 생략 */); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
        return PaymentResult.FromStripe(response); // 결과를 호출자에게 돌려줍니다.
}
}

이 구조는 외부 변동성을 핵심 안으로 들이지 않기 위한 완충 장치다.


6. 사례 5: 애그리거트와 트랜잭션 경계

애그리거트는 함께 일관성을 지켜야 하는 도메인 객체 묶음이다.

예를 들어 주문과 주문 항목이 있다.

Order
  OrderLine
  OrderLine

주문 항목을 추가하면 주문 총액도 맞아야 한다.
주문을 취소하면 항목 상태도 함께 맞아야 한다.

이런 것들은 가까이 두는 편이 낫다.

public class Order // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic AddLine(dynamic product_id, dynamic quantity, decimal price) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        this.Lines.Append(OrderLine(product_id, quantity, price)); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
        this.RecalculateTotal(); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}

애그리거트는 아무 객체나 크게 묶는 것이 아니다.
함께 일관성을 지켜야 하는 것만 묶는다.

너무 크게 묶으면 모든 변경이 큰 덩어리를 잠근다.
너무 작게 나누면 일관성 규칙이 멀리 흩어진다.

애그리거트도 균형이다.


7. 사례 6: 폴더 구조는 설계다

폴더 구조는 단순 정리가 아니다.
개발자가 코드를 어떻게 찾고 이해하는지를 정한다.

기술 역할 기준 구조:

controllers/
  order.py
  payment.py
services/
  order.py
  payment.py
repositories/
  order.py
  payment.py

기능 책임 기준 구조:

orders/
  controller.py
  service.py
  repository.py
payments/
  controller.py
  service.py
  repository.py

기능 변경이 잦다면 기능 기준 구조가 더 읽기 쉽다.
한 기능을 고치기 위해 필요한 파일들이 가까워지기 때문이다.

반대로 기술 계층을 강하게 통제해야 하는 조직에서는 계층형 구조가 더 나을 수도 있다.

정답은 하나가 아니다.
어떤 변경이 자주 일어나는지에 맞춰 가까움을 정해야 한다.


8. 사례 7: 분할 정복과 인터페이스 분리

큰 인터페이스는 사용자를 불필요하게 강하게 묶는다.

public class UserService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public void FindUser(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void UpdateProfile(int id, object data) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void ResetPassword(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void ExportBillingHistory(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
    public void DeleteUser(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}

어떤 화면은 이름 조회만 필요하다.
그런데 전체 UserService를 알아야 한다면 필요 이상의 결합이다.

작은 인터페이스로 나누면 필요한 것만 의존할 수 있다.

public class UserLookup // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public void FindUser(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.

}
public class PasswordReset // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public void ResetPassword(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}

인터페이스 분리는 "작게 나누기" 자체가 목적이 아니다.
사용자가 필요하지 않은 지식에 묶이지 않게 하는 것이다.


9. 사례 8: 도메인 서비스가 모델을 너무 많이 알 때

코드 냄새 하나를 보자.

public class InvoiceService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic SendInvoice(dynamic order) // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        var email = order.Customer.Profile.Contacts.PrimaryEmail; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
        var amount = order.Payment.Summary.FinalAmount; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
        /* 구현 생략 */ // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}

InvoiceService가 주문, 고객, 결제의 내부 구조를 너무 많이 안다.

거리도 멀고 세기도 강하다.
이 구조가 자주 바뀐다면 위험하다.

개선 방향은 필요한 지식을 가까운 곳에 두는 것이다.

public class Order // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
    public dynamic InvoiceRecipient() // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        return this.Customer.PrimaryEmail(); // 결과를 호출자에게 돌려줍니다.

    }
    public dynamic InvoiceAmount() // 이 메서드의 입력과 출력이 공개 약속입니다.
    {
        return this.Payment.FinalAmount(); // 결과를 호출자에게 돌려줍니다.
}
}

서비스는 묻기만 한다.

var email = order.InvoiceRecipient(); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
var amount = order.InvoiceAmount(); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.

이것은 단순한 취향 문제가 아니다.
강한 지식이 너무 먼 곳에 있던 것을 책임 가까이 옮기는 재조정이다.


10. 사례들을 하나로 묶으면

사례 균형 문제 균형 방향
이벤트 소싱 과거 사건 지식이 중요함 사건을 안정적으로 기록
충분히 좋은 통합 추상화 비용이 과할 수 있음 현재 변동성만큼만 분리
수직 슬라이스 기능 변경 파일이 멀리 흩어짐 함께 바뀌는 파일을 가까이
포트와 어댑터 외부 API 변동성이 핵심으로 새어듦 포트 계약과 어댑터로 분리
애그리거트 일관성 규칙이 흩어짐 함께 지켜야 할 규칙을 가까이
폴더 구조 찾는 거리와 변경 거리가 커짐 변경 축에 맞춰 배치
인터페이스 분리 필요 없는 기능까지 의존 필요한 계약만 의존
모델 결합 냄새 내부 구조를 멀리서 앎 책임 가까이 지식을 이동

패턴 이름은 서로 달라도 결합 균형 질문은 같다.


11. 정리

13장은 "패턴 암기"가 아니라 "패턴을 선택하는 이유"를 보여준다.

이벤트 소싱이 좋은 이유는 멋져서가 아니라, 과거 사건 지식이 중요할 때 균형을 맞추기 때문이다.
포트와 어댑터가 좋은 이유는 유명해서가 아니라, 외부 변동성을 핵심에서 멀리 두기 때문이다.
애그리거트가 좋은 이유는 이름이 멋져서가 아니라, 함께 일관성을 지켜야 하는 지식을 가까이 두기 때문이다.

좋은 설계 질문은 늘 비슷하다.

  • 무엇이 자주 바뀌는가?
  • 무엇이 함께 바뀌는가?
  • 무엇이 멀리 떨어져 있는가?
  • 누가 무엇을 너무 많이 알고 있는가?
  • 이 패턴은 지금의 불균형을 실제로 줄이는가?

패턴은 답이 아니라 도구다.
결합 균형이 도구를 고르는 기준이다.


더 해보기

  1. 현재 프로젝트의 폴더 구조가 기술 역할 기준인지 기능 책임 기준인지 살펴보자.
  2. 외부 API 세부사항이 핵심 로직 안에 직접 들어온 곳이 있는지 찾아보자.
  3. 전체 모델을 넘기는 함수 하나를 골라 필요한 값만 넘기도록 바꿀 수 있는지 생각해보자.

연습문제

  1. 설명. 실제 사례로 익히는 균형의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라.
  2. 구분. 두 개념(포트, 어댑터)을 실제 예시 하나로 구분하라.
  3. 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.

부록 A. 쉬운 용어 사전

한글 용어 원문 영문명 아주 쉬운 뜻 이 장에서 나온 위치
포트 Port 내부가 바깥과 소통하기 위해 열어 둔 약속의 입구. 부록 B와 본문 예시
어댑터 Adapter 바깥 기술을 내부 약속에 맞게 바꿔 끼우는 부품. 부록 B와 본문 예시
애그리거트 Aggregate 함께 일관성을 지켜야 하는 도메인 객체 묶음. 부록 B와 본문 예시
트랜잭션 경계 Transaction Boundary 한 번에 성공하거나 실패해야 하는 변경 범위. 부록 B와 본문 예시

부록 B. 헷갈리는 개념 비교표

A B 구분 포인트
포트 어댑터 포트는 내부 약속의 입구, 어댑터는 바깥 기술을 맞춰 끼우는 부품이다.
애그리거트 트랜잭션 경계 애그리거트는 함께 지켜야 할 묶음, 트랜잭션 경계는 한 번에 확정할 범위다.

부록 C. 더 읽을 자료

  • 이 장의 더 해보기 섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다.
  • 같은 책의 0장 한눈에 보기 — 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다.
  • 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
  • 이 장의 flashcards.json — 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.

부록 D. 연습문제 풀이

  1. 설명 예시. 실제 사례로 익히는 균형는 변경이 어디로 번지는지 보고, 필요한 연결과 줄여야 할 연결을 구분하게 해 주는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다.
  2. 구분 예시. 두 개념(포트, 어댑터)의 차이는 이렇게 잡으면 된다. 포트는 내부 약속의 입구, 어댑터는 바깥 기술을 맞춰 끼우는 부품이다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다.
  3. 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플